/* Copyright (C) 2014,2015 Björn Stelter
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package de.hu_berlin.informatik.spws2014.mapever.largeimageview;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.InputDevice;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import de.hu_berlin.informatik.spws2014.mapever.largeimageview.CachedImage.CacheMissResolvedCallback;
public class LargeImageView extends ImageView {
// ////// KEYS FÜR ZU SPEICHERNDE DATEN IM SAVEDINSTANCESTATE-BUNDLE
// Key für das Parcelable, das super.onSaveInstanceState() zurückgibt
private static final String SAVEDVIEWPARCEL = "savedViewParcel";
// Keys für Pan- und Zoomdaten
private static final String SAVEDPANX = "savedPanCenterX";
private static final String SAVEDPANY = "savedPanCenterY";
private static final String SAVEDZOOM = "savedZoomScale";
// ////// CONSTANTS
// Toleranz für Abweichung vom Startpunkt beim Klicken in px
private static final int TOUCH_CLICK_TOLERANCE = 6;
// ////// BITMAP, TILE AND CACHE STUFF
// Statische Bitmap, wird angezeigt, falls kein RegionDecoder initialisiert wurde
private Bitmap staticBitmap = null;
// Last-Recently-Used Cache für Tiles
private CachedImage cachedImage;
// Bildgröße, falls bekannt, sonst -1
private int imageWidth = -1;
private int imageHeight = -1;
// Paint-Objekt, das dazu da ist, das Hintergrundbild transparent zu machen (nur falls not null)
private Paint bgAlphaPaint;
// ////// DISPLAY, PAN- UND ZOOMWERTE
// Aktuelle Pan-Center-Koordinaten und Zoom-Scale
// NOTE: panPos wird jetzt andersherum betrachtet: positiver Wert = die Karte ist nach links/rechts verschoben
// Die Pan-Center-Position gibt jetzt den Punkt an, der im Mittelpunkt des Sichtfeldes liegt.
private float panCenterX = Float.NaN;
private float panCenterY = Float.NaN;
private float zoomScale = 1f;
// Sample-Stufe, wird automatisch aus zoomScale berechnet
// (sampleSize: größer = geringere Auflösung; zoomScale: kleiner = weiter weg vom Bild)
private int sampleSize = 1;
// Minimales und maximales Zoom-Level (Defaultwerte, werden pro Bild neu berechnet)
private float minZoomScale = 0.1f;
private float maxZoomScale = 5.0f;
// ////// TOUCH EVENTS
// Startkoordinaten bei einem Touch-Down-Event um Klicks zu erkennen
private boolean touchCouldBeClick = false;
private float touchStartX;
private float touchStartY;
// SGD behandelt die Zoom-Gesten
private ScaleGestureDetector SGD;
// Hilfsinformationen für Panning und Zooming
private boolean panActive = false;
private int panActivePointerId;
private float panLastTouchX;
private float panLastTouchY;
private boolean panLastTouchIsScaleFocus = false;
// Findet gerade ein Drag-Vorgang statt?
private boolean currentlyDragging = false;
// ////// OVERLAY ICONS
private ArrayList<OverlayIcon> overlayIconList = new ArrayList<OverlayIcon>();
// ////////////////////////////////////////////////////////////////////////
// //////////// CONSTRUCTORS AND INITIALIZATION
// ////////////////////////////////////////////////////////////////////////
public LargeImageView(Context context) {
super(context);
init();
}
public LargeImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public LargeImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* Initialisiert die LargeImageView (wird vom Konstruktor aufgerufen).
*/
private void init() {
// Erstelle SGD, der fürs Zooming zuständig ist
SGD = new ScaleGestureDetector(getContext(), new ScaleListener());
}
@Override
protected Parcelable onSaveInstanceState() {
// Ich weiß nicht, was in dem Parcelable von View drinsteckt, aber ich wills auch nicht einfach wegwerfen...
Parcelable parcel = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putParcelable(SAVEDVIEWPARCEL, parcel);
// Speichere Pan- und Zoom-Werte im State.
bundle.putFloat(SAVEDPANX, panCenterX);
bundle.putFloat(SAVEDPANY, panCenterY);
bundle.putFloat(SAVEDZOOM, zoomScale);
// TODO Können wir irgendwie das Bild/den Stream oder eine Referenz darauf speichern? Und viel
// wichtiger, den Cache? Aktuell muss das Bild beim Drehen immer neu aufgebaut werden... -> #194
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
Bundle bundle = (Bundle) state;
// Ich weiß nicht, was in dem Parcelable von View drinsteckt, aber ich wills auch nicht einfach wegwerfen...
super.onRestoreInstanceState(bundle.getParcelable(SAVEDVIEWPARCEL));
// // Pan und Zoom wiederherstellen
float panX = bundle.getFloat(SAVEDPANX);
float panY = bundle.getFloat(SAVEDPANY);
float zoom = bundle.getFloat(SAVEDZOOM);
setPanZoom(panX, panY, zoom); // calls update()
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d("LIV/onSizeChanged", "w, h, oldw, oldh: " + w + ", " + h + ", " + oldw + ", " + oldh);
if (getWidth() == 0 || getHeight() == 0) {
Log.e("LIV/onSizeChanged", "getWidth() or getHeight() is still zero! (w " + getWidth() + ", h " + getHeight() + ")");
return;
}
// If we have already loaded an image...
if (cachedImage != null || staticBitmap != null) {
onPostLoadImage(true);
}
}
// ////////////////////////////////////////////////////////////////////////
// //////////// LADEN VON BILDERN
// ////////////////////////////////////////////////////////////////////////
/**
* Setzt einen InputStream als Bildquelle. Hierbei wird nach Möglichkeit über CachedImage ein BitmapRegionDecoder
* instanziiert, der Bildteile nach Bedarf lädt, statt das gesamte Bild in eine Bitmap zu laden.
*
* Da der BitmapRegionDecoder nur JPEG und PNG unterstützt, wird bei anderen Formaten (z.B. GIF) sowie im
* Fehlerfall eine IOException geworfen. (Es kann danach versucht werden, das Bild per setImageBitmap statisch
* zu laden.)
*
* @param inputStream
* @throws IOException
*/
public void setImageStream(InputStream inputStream) throws IOException {
// reset image references
cachedImage = null;
staticBitmap = null;
// reset pan
panCenterX = panCenterY = Float.NaN;
// Instanziiere ein CachedImage über den gegebenen InputStream.
// Wirft eine IOException, falls das Bild kein JPEG oder PNG ist, oder ein unerwarteter IO-Fehler auftrat.
cachedImage = new CachedImage(inputStream, new CacheMissResolvedCallback() {
@Override
public void onCacheMissResolved() {
// Wenn nach einem Cache-Miss ein gesuchtes Tile generiert wurde, aktualisiere Ansicht
update();
}
});
// Breite und Höhe des Bildes zwischenspeichern
imageWidth = cachedImage.getWidth();
imageHeight = cachedImage.getHeight();
// Berechnet unter anderem Zoom-Limits
onPostLoadImage(false);
}
/**
* Lädt statisch eine Bitmap als Bildquelle. Statisch bedeutet in diesem Fall, dass es nicht als large image
* von CachedImage behandelt wird, sondern als ganzes Bitmap in die View geladen wird.
*/
@Override
public void setImageBitmap(Bitmap bitmap) {
// Verwende statisch die Bitmap zum Darstellen
cachedImage = null;
staticBitmap = bitmap;
// reset pan
panCenterX = panCenterY = Float.NaN;
if (bitmap != null) {
imageWidth = staticBitmap.getWidth();
imageHeight = staticBitmap.getHeight();
// Berechnet unter anderem Zoom-Limits
onPostLoadImage(false);
}
}
/**
* Gibt eine statisch geladene Bitmap (d.h. nicht gecachtes oder zerteiltes Bild) zurück, wenn vorhanden.
*/
public Bitmap getStaticBitmap() {
return staticBitmap;
}
/**
* Lädt eine Resource als Bildquelle. Hierbei wird nach Möglichkeit über CachedImage ein BitmapRegionDecoder
* instanziiert, indem ein InputStream is erzeugt und setImageStream(is) aufgerufen wird.
*/
@Override
public void setImageResource(int resId) {
try {
// Lade die Resource per Stream
InputStream stream = getResources().openRawResource(resId);
setImageStream(stream);
}
catch (IOException e) {
// Vermutlich schlägt dies fehl, weil die Resource weder JPEG noch PNG ist...
Log.w("LIV/setImageStream", "Can't instantiate CachedImage:");
Log.w("LIV/setImageStream", e.toString());
// Fallback: Lade das Bild statisch als Bitmap (Stream muss neu geöffnet werden)
InputStream stream = getResources().openRawResource(resId);
setImageBitmap(BitmapFactory.decodeStream(stream));
}
}
/**
* Lädt eine Bilddatei per Dateinamen als Bildquelle. Hierbei wird nach Möglichkeit über CachedImage ein
* BitmapRegionDecoder instanziiert, indem ein InputStream is erzeugt und setImageStream(is) aufgerufen wird.
*/
public void setImageFilename(String filename) throws FileNotFoundException {
try {
// Lade das Bild per Stream
InputStream stream = new FileInputStream(filename);
setImageStream(stream);
}
catch (IOException e) {
// Vermutlich schlägt dies fehl, weil die Resource weder JPEG noch PNG ist...
Log.w("LIV/setImageStream", "Can't instantiate CachedImage:");
Log.w("LIV/setImageStream", e.toString());
// Fallback: Lade das Bild statisch als Bitmap (Stream muss neu geöffnet werden)
InputStream stream = new FileInputStream(filename);
setImageBitmap(BitmapFactory.decodeStream(stream));
}
}
/**
* Do not use! Won't be implemented.
*/
@Deprecated
@Override
public void setImageURI(Uri uri) {
// Not implemented because not needed... but we override it to avoid errors if someone does use it.
// Reset stuff
cachedImage = null;
staticBitmap = null;
panCenterX = panCenterY = Float.NaN;
Log.e("LIV/setImageURI", "setImageURI not implemented!");
}
/**
* Wird aufgerufen, nachdem ein Bild geladen wurde. Wenn überschrieben, dann unbedingt super.onPostLoadImage()
* aufrufen, da diese Implementierung z.B. noch Zoom-Limits berechnet und das Bild zentriert.
*/
protected void onPostLoadImage(boolean calledByOnSizeChanged) {
// If called before onLayout() we don't know width and height yet... so we have to call this method later
// in onSizeChanged again.
if (getWidth() != 0 && getHeight() != 0) {
// (re-)calculate MIN_ and MAX_ZOOM_SCALE
calculateZoomScaleLimits();
// If no pan has been set yet (just loaded): center image and zoom out until whole image is visible
if (Float.isNaN(panCenterX) || Float.isNaN(panCenterY)) {
setPanZoomFitImage();
}
}
else {
Log.d("LIV/onPostLoadImage", "Couldn't execute onPostLoadImage yet, no width/height known!");
}
}
// ////////////////////////////////////////////////////////////////////////
// //////////// IMAGE PROPERTIES
// ////////////////////////////////////////////////////////////////////////
/**
* Gibt die Breite des Bildes zurück. (Tatsächliche Bildgröße, auch wenn nur kleinere Teile geladen sind.)
*/
public int getImageWidth() {
return imageWidth;
}
/**
* Gibt die Höhe des Bildes zurück. (Tatsächliche Bildgröße, auch wenn nur kleinere Teile geladen sind.)
*/
public int getImageHeight() {
return imageHeight;
}
/**
* Gibt Transparenz des Bildes zurück ("Background" um Verwechslung mit setImageAlpha() zu vermeiden,
* im Gegensatz zu Foreground, was dann die OverlayIcons wären).
*
* @return Wert von 0 (vollkommen transparent) bis 255 (undurchsichtig).
*/
public int getBackgroundAlpha() {
return bgAlphaPaint == null ? 255 : bgAlphaPaint.getAlpha();
}
/**
* Setze Transparenz des angezeigten Bildes ("Foreground" um Verwechslung mit setImageAlpha() zu vermeiden,
* nicht Background, um Verwechslung mit setBackgroundColor() zu vermeiden... alles sehr verwirrend).
*
* @param newAlpha Wert von 0 (vollkommen transparent) bis 255 (undurchsichtig).
*/
public void setForegroundAlpha(int newAlpha) {
bgAlphaPaint = new Paint();
bgAlphaPaint.setAlpha(newAlpha);
}
// ////////////////////////////////////////////////////////////////////////
// //////////// PANNING UND ZOOMING
// ////////////////////////////////////////////////////////////////////////
// ////// GETTERS AND SETTERS
/** Gibt aktuelle Pan-Center-X-Koordinate (Bildpunkt, der im Sichtfeld zentriert wird) zurück. */
public float getPanCenterX() {
return panCenterX;
}
/** Gibt aktuelle Pan-Center-Y-Koordinate (Bildpunkt, der im Sichtfeld zentriert wird) zurück. */
public float getPanCenterY() {
return panCenterY;
}
/** Gibt aktuelle Pan-Center-Koordinaten (Bildpunkt, der im Sichtfeld zentriert wird) als PointF zurück. */
public PointF getPanCenter() {
return new PointF(panCenterX, panCenterY);
}
/** Setzt neue Pan-Center-Koordinaten (Bildpunkt, der im Sichtfeld zentriert wird). */
public void setPanCenter(float newX, float newY) {
panCenterX = newX;
panCenterY = newY;
update();
}
/** Setzt neue Pan-Center-Koordinaten (Bildpunkt, der im Sichtfeld zentriert wird). */
public void setPanCenter(PointF point) {
if (point == null) {
panCenterX = panCenterY = 0;
}
else {
panCenterX = (float) point.x;
panCenterY = (float) point.y;
}
update();
}
/** Gibt aktuelles Zoom-Level zurück. (Je kleiner, desto weiter weg ist das Bild.) */
public float getZoomScale() {
return zoomScale;
}
/** Gibt aktuelle Sample-Stufe zurück. (Je größer, desto geringer ist die Auflösung des Bildes.) */
public int getSampleSize() {
return sampleSize;
}
/** Setzt neues Zoom-Level und berechnet Sample-Stufe neu. */
public void setZoomScale(float newZoomScale) {
zoomScale = newZoomScale;
// Zoom-Level darf Minimum und Maximum nicht unter-/überschreiten
if (zoomScale < minZoomScale || zoomScale > maxZoomScale) {
zoomScale = Math.max(minZoomScale, Math.min(zoomScale, maxZoomScale));
}
// SampleSize neuberechnen
sampleSize = calculateSampleSize(zoomScale);
update();
}
/**
* Setzt neue Pan-Center-Koordinaten (Bildpunkt, der im Sichtfeld zentriert wird) und neues Zoom-Level
* (und berechnet Sample-Stufe neu).
*/
public void setPanZoom(float newX, float newY, float newZoomScale) {
panCenterX = newX;
panCenterY = newY;
setZoomScale(newZoomScale); // calls update()
}
/** Zentriert den Bildmittelpunkt, indem das Pan-Center auf diesen gesetzt wird. */
public void setPanCenterToImageCenter() {
float centerX = imageWidth / 2;
float centerY = imageHeight / 2;
setPanCenter(centerX, centerY); // calls update()
}
/**
* Zentriert das Bild und setzt die Zoomstufe so, dass das ganze Bild sichtbar ist. Es wird also herausgezoomt,
* bis kein Teil des Bildes mehr abgeschnitten wird, eventuell mit Letterbox/Pillarbox, aber es wird nicht
* herangezoomt.
*/
public void setPanZoomFitImage() {
if (getWidth() == 0 || getHeight() == 0 || imageWidth <= 0 || imageHeight <= 0) {
Log.w("LIV/setPanZoomFitImage", "Some dimensions are still unknown: getWidth/getHeight: " + getWidth() +
"/" + getHeight() + ", imageWidth/imageHeight: " + imageWidth + "/" + imageHeight);
return;
}
// Bild zentrieren und so weit rauszoomen, dass das ganze Bild sichtbar ist (nicht jedoch das Bild abschneiden
// oder heranzoomen)
float centerX = imageWidth / 2;
float centerY = imageHeight / 2;
float fitZoomScale = Math.min((float) getWidth() / imageWidth, (float) getHeight() / imageHeight);
fitZoomScale = Math.min(1, fitZoomScale);
setPanZoom(centerX, centerY, fitZoomScale); // calls update()
}
// ////// SAMPLESIZE UND MAX/MIN ZOOM SCALE BERECHNUNG
/**
* Berechnet die Sample-Stufe zu einer Zoom-Stufe. Dies ist dabei die größte Zweierpotenz, die <= 1/scale ist.
*
* @param scale Zoom-Stufe
* @return Sample-Stufe
*/
public static int calculateSampleSize(float scale) {
int sample = 1;
// bilde ganzzahligen Kehrwert von scale (kann für scale < 1 null werden)
int x = (int) (1.0 / scale);
// Das Sampling Level ist die größte Zweierpotenz, die <= 1/scale ist.
// Wir finden diese, indem wir x durch 2 teilen und samplingLevel verdoppeln, bis x = 0 ist.
// z.B. x=9:
// x=9, s=1 --> x=4, s=2 --> x=2, s=4 --> x=1, s=8 --> x=0, s=8.
while ((x /= 2) > 0) {
sample *= 2;
}
// Begrenze Samplesize auf 32 (sollte ausreichen)
if (sample > 32) {
sample = 32;
}
return sample;
}
/**
* Berechnet optimale Zoom-Grenzen.
*/
private void calculateZoomScaleLimits() {
if (getWidth() == 0 || getHeight() == 0 || imageWidth <= 0 || imageHeight <= 0) {
Log.w("LIV/calcZoomScaleLimits", "Some dimensions are still unknown: getWidth/getHeight: " + getWidth() +
"/" + getHeight() + ", imageWidth/imageHeight: " + imageWidth + "/" + imageHeight);
return;
}
// Wie groß ist der Bildschirm relativ zur Karte?
double relativeWidth = ((double) getWidth()) / imageWidth;
double relativeHeight = ((double) getHeight()) / imageHeight;
// Man kann nur soweit rauszoomen, dass die ganze Karte und noch etwas Rand auf den Bildschirm passt.
minZoomScale = (float) (0.8 * Math.min(1, Math.min(relativeHeight, relativeWidth)));
// Man kann bei hinreichend großen Bildern auf 6x ranzoomen, bei sehr kleinen Bildern maximal so, dass
// sie den Bildschirm ausfüllen.
maxZoomScale = (float) Math.max(6.0, Math.max(relativeHeight, relativeWidth));
}
// ////// POSITIONSUMRECHNUNGEN
/**
* Gibt zu einer Bildschirmposition die (aktuelle) Bildposition zurück.
*/
public PointF screenToImagePosition(float screenX, float screenY) {
if (Float.isNaN(panCenterX) || Float.isNaN(panCenterY) || getWidth() == 0 || getHeight() == 0) {
Log.w("LIV/screenToImgPosition", "Either panCenter is not initialized (" + panCenterX + ", " + panCenterY
+ ") or view dimensions are still zero (getWidth/getHeight: " + getWidth() + "/" + getHeight() + ")");
return null;
}
// Der Offset berechnet sich aus PanCenterPos und halber (scale-gewichteter) Viewgröße.
float imageX = panCenterX + (-getWidth() / 2 + screenX) / zoomScale;
float imageY = panCenterY + (-getHeight() / 2 + screenY) / zoomScale;
return new PointF(imageX, imageY);
}
/**
* Gibt zu einer Bildposition die (aktuelle) Bildschirmposition zurück.
*/
public PointF imageToScreenPosition(float imageX, float imageY) {
if (Float.isNaN(panCenterX) || Float.isNaN(panCenterY) || getWidth() == 0 || getHeight() == 0) {
Log.w("LIV/screenToImgPosition", "Either panCenter is not initialized (" + panCenterX + ", " + panCenterY
+ ") or view dimensions are still zero (getWidth/getHeight: " + getWidth() + "/" + getHeight() + ")");
return null;
}
// Umkehrfunktion zu screenToImagePosition()
float screenX = (imageX - panCenterX) * zoomScale + getWidth() / 2;
float screenY = (imageY - panCenterY) * zoomScale + getHeight() / 2;
return new PointF(screenX, screenY);
}
// ////// EVENT HANDLERS
/**
* Wird getriggert, wenn sich Pan-Position oder Zoom durch ein Touch-Event ändern. Tut nichts, kann aber von
* Subklassen überschrieben werden.
*/
protected void onTouchPanZoomChange() {
return;
}
/**
* Wird getriggert, wenn ein Klick auf eine bestimmte Bildschirmposition stattfindet. Tut nichts, kann aber von
* Subklassen überschrieben werden. Um Bildschirmkoordinaten in Bildkoordinaten umzuwandeln siehe
* {@link #screenToImagePosition(float, float)}.
*
* @param clickX
* @param clickY
* @return true, falls das Event behandelt wurde.
*/
protected boolean onClickPosition(float clickX, float clickY) {
return false;
}
// ////////////////////////////////////////////////////////////////////////
// //////////// OVERLAY ICONS
// ////////////////////////////////////////////////////////////////////////
/**
* Fügt ein OverlayIcon der Liste hinzu, sodass dieses in onDraw() gezeichnet wird. (Inklusive update())
*/
public void attachOverlayIcon(OverlayIcon icon) {
overlayIconList.add(icon);
update();
}
/**
* Entfernt ein OverlayIcon aus der Liste, sodass dieses nicht mehr gezeichnet wird. (Inklusive update())
*/
public void detachOverlayIcon(OverlayIcon icon) {
overlayIconList.remove(icon);
update();
}
// ////////////////////////////////////////////////////////////////////////
// //////////// TOUCH AND CLICK EVENT HANDLING
// ////////////////////////////////////////////////////////////////////////
// To allow zooming with keyboard (e.g. emulator)
@Override
public boolean onKeyDown(int code, KeyEvent event) {
if (code == KeyEvent.KEYCODE_COMMA) {
setZoomScale(zoomScale * 2);
return true;
} else if (code == KeyEvent.KEYCODE_PERIOD) {
setZoomScale(zoomScale / 2);
return true;
}
return super.onKeyDown(code, event);
}
// For zooming via mouse scrollwheel
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 &&
event.getAction() == MotionEvent.ACTION_SCROLL &&
event.getAxisValue(MotionEvent.AXIS_VSCROLL) != 0) {
float factor = event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0 ? 0.90f : 1.10f;
// Calculate pan offsetting.
float focusX = (event.getX() - getWidth() / 2) / zoomScale;
float focusY = (event.getY() - getHeight() / 2) / zoomScale;
float dx = focusX * (1 - factor);
float dy = focusY * (1 - factor);
float new_x = Float.isNaN(panCenterX) ? -dx : panCenterX - dx;
float new_y = Float.isNaN(panCenterY) ? -dy : panCenterY - dy;
setPanZoom(new_x, new_y, zoomScale * factor);
return true;
}
return super.onGenericMotionEvent(event);
}
// ////// MAIN ONTOUCHEVENT
/**
* Behandelt Touchgesten, primär Klickerkennung, Panning und Zooming.
* Kann überschrieben werden, um eigene Touchgesten zu implementieren. Soll Pan und Zoom vermieden werden, aber
* Klicks dennoch erkannt werden, rufe super.onTouchEvent_clickDetection(event) auf, in welche die Klickerennung
* ausgelagert wurde.
*
* @return true, falls Event behandelt wurde, sonst false. (Hier: eigentlich immer true.)
*/
// Die Lint-Warnung "onTouchEvent should call performClick when a click is detected" wird fälschlicherweise(?)
// angezeigt, obwohl onTouchEvent() onTouchEvent_clickDetection() aufruft, welche wiederum performClick() aufruft.
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
// ////// KLICKERKENNUNG
// Wenn Klick erkannt wurde, wurde das Event behandelt.
if (onTouchEvent_clickDetection(event)) {
return true;
}
// ////// OVERLAYICON DRAG AND DROP
// Wenn Drag/Drop erkannt wurde, wurde das Event behandelt.
boolean dragAndDropHandled = onTouchEvent_dragAndDrop(event);
// ////// PANNING UND ZOOMING
// (auch bei true von onTouchEvent_dragAndDrop() aufrufen, weil manche panZoom-Events ausgeführt werden müssen)
onTouchEvent_panZoom(event, dragAndDropHandled);
return true;
}
// ////// PANNING AND ZOOMING
/**
* Führt das Panning und Zooming durch. Wird als Teil von onTouchEvent aufgerufen.
*
* @return true. Auch Overrides sollten stets true zurückgeben (sonst kommen folgende Touch-Events nicht mehr an).
*/
public boolean onTouchEvent_panZoom(MotionEvent event, boolean dragAndDropHandled) {
// Was für ein MotionEvent wurde detektiert?
int action = event.getActionMasked();
// Behandlung des Panning
switch (action) {
case MotionEvent.ACTION_DOWN: {
// Erste Berührung des Touchscreens (mit einem Finger)
// Auch wenn wir gerade einen laufenden Drag-Vorgang haben, Pan-Start behandeln, da der
// Drag-Vorgang abgebrochen werden könnte.
// Position des ersten Pointers (Fingers) ermitteln
final int pointerIndex = event.getActionIndex();
final float x = event.getX(pointerIndex);
final float y = event.getY(pointerIndex);
// Position der Pan-Bewegung merken
panLastTouchX = x;
panLastTouchY = y;
// Pointer-ID merken
panActivePointerId = event.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
// Bewegung der Finger während Berührung
// Nur behandeln, wenn wir einen aktiven Pan-Finger haben
if (panActivePointerId == -1)
return true;
// Position des aktuellen Pointers ermitteln
final int pointerIndex = event.findPointerIndex(panActivePointerId);
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
// Führe Panning nur dann durch, wenn das Event sicher kein Klick ist. Das verhindert kleine
// Bewegungen des Bildes beim Klicken.
if (touchCouldBeClick) {
return true;
}
// Wenn wir gerade Zoomen, richtet sich das Panning nach dem ScaleFocus
if (panLastTouchIsScaleFocus) {
if (!SGD.isInProgress()) {
// Zoomvorgang beendet, also wieder nach einzelnem Finger pannen
panLastTouchIsScaleFocus = false;
panLastTouchX = x;
panLastTouchY = y;
break;
}
else {
x = SGD.getFocusX();
y = SGD.getFocusY();
}
}
// Bewegungsdistanz errechnen
final float dx = x - panLastTouchX;
final float dy = y - panLastTouchY;
// Aktuelle Position für nächstes move-Event merken
panLastTouchX = x;
panLastTouchY = y;
// Nur, falls wir gerade keinen laufenden Drag-Vorgang haben, das Panning tatsächlich ändern
if (!currentlyDragging) {
// Jetzt pannen wir "offiziell" (ab jetzt keine Drag-Vorgänge mehr starten)
panActive = true;
// Falls wir noch keine Pan-Werte haben (?) initialisiere sie mit 0
if (Float.isNaN(panCenterX))
panCenterX = 0;
if (Float.isNaN(panCenterY))
panCenterY = 0;
// Panning Geschwindigkeit wird an die aktuelle Skalierung angepasst
panCenterX -= dx / zoomScale;
panCenterY -= dy / zoomScale;
// Event auslösen, dass Pan/Zoom durch Touchevent verändert wurden
onTouchPanZoomChange();
}
break;
}
case MotionEvent.ACTION_UP:
// Der letzte Finger wird gehoben
panActivePointerId = -1;
panActive = false;
break;
case MotionEvent.ACTION_POINTER_DOWN:
// Ein weiterer Finger berührt das Touchscreen
break;
case MotionEvent.ACTION_POINTER_UP: {
// Ein Finger verlässt das Touchscreen, aber es sind noch Finger auf dem Touchscreen.
// Welcher Finger wurde entfernt?
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == panActivePointerId) {
// der "aktive" Finger wurde entfernt, wähle neuen und passe gemerkte Koordinaten an
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
panLastTouchX = event.getX(newPointerIndex);
panLastTouchY = event.getY(newPointerIndex);
panActivePointerId = event.getPointerId(newPointerIndex);
}
break;
}
}
// Behandlung vom Zoomen
if (!currentlyDragging) {
SGD.onTouchEvent(event);
}
// Position/Skalierung der ImageView anpassen
update();
return true;
}
// ////// SCALELISTENER: implementiert die onScale-Methode des SGD und kümmert sich damit um den Zoom.
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
// Wenn wir scalen, haben wir definitiv kein Klick-Event mehr.
touchCouldBeClick = false;
// Damit das Panning während des Zooms sich nicht nur auf einen
// Finger beschränkt, panne hier anhand des Focus-Punktes.
if (!panLastTouchIsScaleFocus) {
panLastTouchX = detector.getFocusX();
panLastTouchY = detector.getFocusY();
panLastTouchIsScaleFocus = true;
}
// Zoom-Level aktualisieren
float oldScale = zoomScale;
float scaleFactor = detector.getScaleFactor();
zoomScale *= scaleFactor;
// Zoom-Level darf Minimum und Maximum nicht unter-/überschreiten
if (zoomScale < minZoomScale || zoomScale > maxZoomScale) {
zoomScale = Math.max(minZoomScale, Math.min(zoomScale, maxZoomScale));
// scaleFactor wird unten noch mal benötigt, also anpassen
scaleFactor = zoomScale / oldScale;
}
// Um den Zoom-Focus (der Mittelpunkt zwischen den Fingern) beizubehalten, ist eine
// zusätzliche Translation erforderlich.
// (Vermeide Pivot-Parameter von Matrix.setScale(), da dies mit der Translation durchs
// Panning kollidiert... so ist es einfacher.)
// Berechne Fokus-Koordinaten relativ zum Pan-Center und zur Zoomscale
float focusX = (detector.getFocusX() - getWidth() / 2) / zoomScale;
float focusY = (detector.getFocusY() - getHeight() / 2) / zoomScale;
// Durch Zoom wird Focus verschoben, hierdurch wird die Verschiebung rückgängig gemacht.
float dx = focusX * (1 - scaleFactor);
float dy = focusY * (1 - scaleFactor);
if (Float.isNaN(panCenterX))
panCenterX = 0;
if (Float.isNaN(panCenterY))
panCenterY = 0;
// Verschiebe das Panning
panCenterX -= dx;
panCenterY -= dy;
// SampleSize neuberechnen
sampleSize = calculateSampleSize(zoomScale);
// Event auslösen, dass Pan/Zoom durch Touchevent verändert wurden
onTouchPanZoomChange();
return true;
}
}
// ////// CLICK DETECTION
/**
* Führt die Klickerkennung durch. Wird von onTouchEvent aufgerufen, welches nur dann weitermacht, wenn hier false
* zurückgegeben wird (d.h. wenn kein Klick detektiert wurde).
* Die Klickerkennung wurde ausgelagert, damit Subklassen onTouchEvent überschreiben und damit das Panning
* deaktivieren, aber dennoch per Aufruf von super.onTouchEvent_clickDetection() Klicks erkennen lassen können.
*/
public boolean onTouchEvent_clickDetection(MotionEvent event) {
// Was für ein MotionEvent wurde detektiert?
int action = event.getActionMasked();
// Falls mehrere Finger das Touchscreen berühren, kann es kein Klick sein.
if (event.getPointerCount() > 1) {
touchCouldBeClick = false;
return false;
}
// Position des Fingers ermitteln
// final int pointerIndex = event.getActionIndex();
final float x = event.getX();
final float y = event.getY();
// Behandlung des Panning
switch (action) {
case MotionEvent.ACTION_DOWN:
// Erste Berührung des Touchscreens: Speichere Startposition.
// (Falls wir uns zu weit davon wegbewegen, wollen wir keinen Klick auslösen.)
touchCouldBeClick = true;
touchStartX = x;
touchStartY = y;
break;
case MotionEvent.ACTION_MOVE:
// Bewegung der Finger während Berührung
if (touchCouldBeClick) {
// Falls wir uns zu weit vom Startpunkt der Geste wegbewegen, ist es kein Klick mehr.
if (Math.abs(x - touchStartX) > TOUCH_CLICK_TOLERANCE || Math.abs(y - touchStartY) > TOUCH_CLICK_TOLERANCE) {
touchCouldBeClick = false;
}
}
break;
case MotionEvent.ACTION_UP:
// Der letzte Finger wird gehoben
// Klick auslösen, falls das Event als Klick interpretiert wurde (d.h. nur ein Finger und vom
// Startpunkt nicht weiter als TOUCH_CLICK_TOLERANCE Pixel entfernt).
if (touchCouldBeClick) {
touchCouldBeClick = false;
performClick();
return true;
}
break;
}
// Event wurde nicht behandelt, reiche es an (den Rest von) onTouchEvent weiter.
return false;
}
@Override
public boolean performClick() {
Log.d("LIV/performClick", "Click on screen position " + touchStartX + ", " + touchStartY + " detected!");
// Prüfe, ob ein OverlayIcon angeklickt wurde und führe gegebenenfalls dessen onClick-Methode aus.
for (OverlayIcon icon : overlayIconList) {
// Icon überspringen, falls es keine Hitbox hat
if (icon.getTouchHitbox() == null)
continue;
// Bildschirmposition des Icons (ohne Offset) errechnen
PointF screenPoint = imageToScreenPosition(icon.getImagePositionX(), icon.getImagePositionY());
// Position des Klicks relativ zum Icon
int relativeX = (int) (touchStartX - screenPoint.x);
int relativeY = (int) (touchStartY - screenPoint.y);
// war der Klick innerhalb der Icon-Hitbox?
if (icon.getTouchHitbox().contains(relativeX, relativeY)) {
// Falls eine Drag-Bewegung gestartet wurde, muss diese abgebrochen werden.
if (icon.getDragPointerID() > -1) {
icon.onDragUp(touchStartX, touchStartY);
}
// Führe onClick-Event aus. Return, falls onClick das Event behandelt hat.
if (icon.onClick(touchStartX, touchStartY) == true)
return true;
}
}
// onClickPosition-Event auslösen. Falls es true zurückgibt, wurde das Event behandelt...
if (onClickPosition(touchStartX, touchStartY) == true)
return true;
// ... ansonsten Standard-Handler ausführen, der dann andere onClick-Events triggert.
return super.performClick();
}
// ////// OVERLAY ICON DRAG AND DROP
/**
* Führt die Erkennung von Drag- und Drop-Events von OverlayIcons durch. Wird von onTouchEvent aufgerufen, welches
* nur dann weitermacht, wenn hier false zurückgegeben wird (d.h. wenn für den aktuellen Finger kein Drag and Drop
* detektiert wurde).
*/
public boolean onTouchEvent_dragAndDrop(MotionEvent event) {
// Was für ein MotionEvent wurde detektiert?
int action = event.getActionMasked();
// Position des ersten Pointers (Fingers) ermitteln
final int pointerIndex = event.getActionIndex();
final float x = event.getX(pointerIndex);
final float y = event.getY(pointerIndex);
final int pointerID = event.getPointerId(pointerIndex);
// Behandlung des Drag and Drops
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// Ein (erster oder weiterer) Finger berührt das Touchscreen
// Drag and Drop nur dann, wenn wir gerade nicht mitten beim Panning oder Zooming sind.
if (panActive) {
return false;
}
// Prüfe, ob ein OverlayIcon berührt wurde und trigger ggf. dessen onDragDown-Event.
for (OverlayIcon icon : overlayIconList) {
// Icon überspringen, falls es keine Hitbox hat
if (icon.getTouchHitbox() == null)
continue;
// Bildschirmposition des Icons (ohne Offset) errechnen
PointF screenPoint = imageToScreenPosition(icon.getImagePositionX(), icon.getImagePositionY());
// Position des Klicks relativ zum Icon
int relativeX = (int) (x - screenPoint.x);
int relativeY = (int) (y - screenPoint.y);
// war der Klick innerhalb der Icon-Hitbox?
if (icon.getTouchHitbox().contains(relativeX, relativeY)) {
// Ja, führe onDragDown-Event aus. Return, falls onClick das Event behandelt hat.
if (icon.onDragDown(pointerID, x, y) == true) {
// Merken, dass ein Drag-Vorgang läuft (Extra-Test, da onDragDown() z.B. auch laufende
// Drag-Vorgänge abbrechen könnte (gilt als behandeltes Event))
if (icon.getDragPointerID() != -1) {
currentlyDragging = true;
}
return true;
}
}
}
break;
case MotionEvent.ACTION_MOVE:
// Bewegung eines Fingers während Berührung
// Drag and Drop nur dann, wenn das Event definitiv kein Klick ist.
if (touchCouldBeClick) {
return false;
}
// Falls gerade gar kein Drag-Vorgang läuft, können wir hier auch abbrechen
if (!currentlyDragging) {
return false;
}
boolean handledDragAndDrop = false;
// Prüfe für jeden Pointer (=Finger)...
for (int i = 0; i < event.getPointerCount(); i++) {
// ... und für jedes OverlayIcon...
for (OverlayIcon icon : overlayIconList) {
// ... ob der Finger das OverlayIcon draggt...
// (Falls nicht gedraggt, gibt die Methode -1 zurück)
if (icon.getDragPointerID() == event.getPointerId(i)) {
// ... falls ja, führe onDragMove-Event aus
if (icon.onDragMove(event.getX(i), event.getY(i)) == true)
handledDragAndDrop = true;
}
}
}
// true zurückgeben, falls wir mindestens ein Drag- and Drop-Event gehandlet haben
return handledDragAndDrop;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// Ein Finger verlässt das Touchscreen
// Falls gerade gar kein Drag-Vorgang läuft, können wir hier auch abbrechen
if (!currentlyDragging) {
return false;
}
// Counter, wie viele Icons (im Fall von Multitouch) noch gedraggt werden.
int stillDraggedCount = 0;
boolean returnvalue = false;
// Prüfe, ob der Finger eines der OverlayIcons draggt und trigger ggf. dessen onDragUp-Event.
for (OverlayIcon icon : overlayIconList) {
// Wird das Icon gerade gedraggt?
if (icon.getDragPointerID() != -1) {
stillDraggedCount++;
// Wird es vom aktuellen Finger gedraggt?
if (icon.getDragPointerID() == pointerID) {
// Ja, führe onDragUp-Event aus. (Ignoriere return value)
icon.onDragUp(x, y);
stillDraggedCount--;
returnvalue = true;
}
}
}
// Falls nun kein Icon mehr gedraggt wird, Variable zurücksetzen
if (stillDraggedCount == 0) {
currentlyDragging = false;
}
if (returnvalue == true)
return true;
break;
}
// Event wurde nicht behandelt, reiche es an (den Rest von) onTouchEvent weiter.
return false;
}
/**
* Gibt true zurück, falls gerade ein OverlayIcon gedraggt wird.
*/
public boolean isCurrentlyDragging() {
return currentlyDragging;
}
/**
* Bricht alle laufenden Dragvorgänge ab.
*/
public void cancelAllDragging() {
for (OverlayIcon icon : overlayIconList) {
// Wird das Icon gerade gedraggt?
if (icon.getDragPointerID() != -1) {
// Drag-Vorgang abbrechen
icon.onDragUp(Float.NaN, Float.NaN);
}
}
currentlyDragging = false;
}
// ////////////////////////////////////////////////////////////////////////
// //////////// DARSTELLUNG DES BILDES
// ////////////////////////////////////////////////////////////////////////
/**
* Aktualisiert die Darstellung. Wird aufgerufen, wenn Pan-Position oder Zoomlevel verändert werden.
*/
public void update() {
// Begrenze das Panning, so dass man das Bild nicht beliebig weit aus der Bildfläche schieben kann.
if (imageWidth > 0 && imageHeight > 0) {
// Panning so begrenzen, dass PanCenter nicht die Bildgrenzen verlassen kann. (Simple, huh?)
// Linke Begrenzung
if (panCenterX < 0)
panCenterX = 0;
// Rechte Begrenzung
else if (panCenterX >= imageWidth)
panCenterX = imageWidth;
// Obere Begrenzung
if (panCenterY < 0)
panCenterY = 0;
// Untere Begrenzung
else if (panCenterY >= imageHeight)
panCenterY = imageHeight;
// Log.d("LIV/update", "Pan center: " + panCenterX + "/" + panCenterY + ", zoom: " + zoomScale);
}
// View neu zeichnen lassen (onDraw)
// TODO ausprobieren, ob mehrere invalidate-Aufrufe in Folge onDraw auch mehrfach aufrufen
// .... (da manche Methoden implizit mehrmals update() aufrufen :S)
this.invalidate();
}
/**
* Returns true if everything is ready to call onDraw (pan set, getWidth/Height return non-zero values, etc.).
* Check this if you override onDraw!
*/
protected boolean isReadyToDraw() {
if (Float.isNaN(panCenterX) || Float.isNaN(panCenterY) || getWidth() == 0 || getHeight() == 0) {
return false;
}
return true;
}
/**
* Zeichnet das Bild, gegebenenfalls in Einzelteilen, sowie die OverlayIcons.
* Siehe auch {@link #onDraw_cachedImage(Canvas)}, {@link #onDraw_staticBitmap(Canvas)},
* {@link #onDraw_overlayIcons(Canvas)}.
*/
@Override
protected void onDraw(Canvas canvas) {
if (!isReadyToDraw()) {
return;
}
// ZoomScale darf nicht 0 sein -- wird es eigentlich auch nie, aber für den Fall der Fälle...
if (zoomScale == 0) {
zoomScale = (float) 1.0 / 256; // dürfte klein genug sein :P
}
canvas.save();
// Prüfe, ob wir ein CachedImage oder ein statisches Bitmap verwenden
if (cachedImage != null) {
onDraw_cachedImage(canvas);
}
else {
if (!onDraw_staticBitmap(canvas)) {
// Fallback (setBackgroundAlpha funktioniert hiermit nicht... naja.)
super.onDraw(canvas);
}
}
canvas.restore();
// Overlay-Icons zeichnen
canvas.save();
onDraw_overlayIcons(canvas);
canvas.restore();
}
/**
* Übernimmt den Teil von {@link #onDraw(Canvas)}, der ein gecachtes Bild (in Tiles) anzeigt.
*/
protected void onDraw_cachedImage(Canvas canvas) {
// Effektive Skalierung berechnen (Skalierung, die nach dem Sampling noch erforderlich ist)
float effectiveScale = sampleSize * zoomScale;
// Viewport berechnen: sichtbarer Bildausschnitt (relativ zum gesampelten Bild; mit Rand)
int viewportWidth = (int) (getWidth() / effectiveScale);
int viewportHeight = (int) (getHeight() / effectiveScale);
// Viewportgrenzen: PanCenter bei aktueller SampleSize - 1/2 * Viewport, weil PanCenter
int viewportLeft = (int) (panCenterX / sampleSize) - viewportWidth / 2;
int viewportTop = (int) (panCenterY / sampleSize) - viewportHeight / 2;
int viewportRight = viewportLeft + viewportWidth;
int viewportBottom = viewportTop + viewportHeight;
// Log.d("LIV/onDraw_cachedImage", "sample: " + sampleSize + ", viewport: " + viewportWidth + "x" +
// viewportHeight + ", (l,t,r,b) = ("
// + viewportLeft + "," + viewportTop + "," + viewportRight + "," + viewportBottom + ")");
// Startkoordinaten für die Zeichnen-Schleife
// (Linksoberstes Tile beginnt i.A. weiter links oben als der Viewport)
int startX = viewportLeft - viewportLeft % CachedImage.TILESIZE;
int startY = viewportTop - viewportTop % CachedImage.TILESIZE;
// Nicht versuchen, etwas links/oben vom Bild zu zeichnen
if (startX < 0)
startX = 0;
if (startY < 0)
startY = 0;
// Zeichenbereich skalieren und verschieben (effektive Skalierung nach dem Sampling)
canvas.scale(effectiveScale, effectiveScale);
canvas.translate(-viewportLeft, -viewportTop);
// Zeilenweise Tiles zeichnen, bis am Viewportrand oder Bildrand angekommen
for (int y = startY; y < viewportBottom && y < imageHeight / sampleSize; y += CachedImage.TILESIZE) {
for (int x = startX; x < viewportRight && x < imageWidth / sampleSize; x += CachedImage.TILESIZE) {
// Unsere Koordinaten sind abhängig vom Sampling. Das gesuchte Tile beginnt also nicht
// bei (x,y) sondern bei samplingLevel*(x,y), wird aber an (x,y) gezeichnet.
Bitmap bm = cachedImage.getTileBitmap(sampleSize * x, sampleSize * y, sampleSize);
// Log.d("LIV/onDraw_cachedImage", "Drawing tile " + getCacheKey(sampleSize * x, sampleSize * y,
// sampleSize)
// + (bm == null ? " ... null" : (" at " + x + "," + y)));
// Tile zeichnen, falls es bereits existiert (also im Cache gefunden wurde)
if (bm != null) {
canvas.drawBitmap(bm, x, y, bgAlphaPaint);
}
}
}
}
/**
* Übernimmt den Teil von {@link #onDraw(Canvas)}, der ein statisches (nicht gecachtes) Bild anzeigt.
*
* @param canvas
* @return False, falls auch kein staticBitmap vorhanden ist... Benutze super.onDraw().
*/
protected boolean onDraw_staticBitmap(Canvas canvas) {
// Viewport berechnen: sichtbarer Bildausschnitt (relativ zum Bild)
int viewportWidth = (int) (getWidth() / zoomScale);
int viewportHeight = (int) (getHeight() / zoomScale);
// Viewportgrenzen: PanCenter - 1/2 * Viewport
int viewportLeft = (int) panCenterX - viewportWidth / 2;
int viewportTop = (int) panCenterY - viewportHeight / 2;
// Log.d("LIV/onDraw_staticBitmap", "width, height: " + getWidth() + "/" + getHeight());
// Skalieren und verschieben
canvas.scale(zoomScale, zoomScale);
canvas.translate(-viewportLeft, -viewportTop);
if (staticBitmap != null) {
// Bitmap statisch anzeigen
canvas.drawBitmap(staticBitmap, 0, 0, bgAlphaPaint);
}
else {
return false;
}
return true;
}
/**
* Übernimmt den Teil von {@link #onDraw(Canvas)}, der die OverlayIcons zeichnet.
*/
protected void onDraw_overlayIcons(Canvas canvas) {
// Nichts tun, falls keine Overlay Icons vorhanden
if (overlayIconList.isEmpty())
return;
// Bildursprung relativ zu Bildschirmkoordinaten berechnen
float imageOriginX = -panCenterX * zoomScale + getWidth() / 2;
float imageOriginY = -panCenterY * zoomScale + getHeight() / 2;
// Log.d("LIV/onDraw_overlayIcons", "width, height: " + getWidth() + "/" + getHeight() + ", image origin " +
// imageOriginX + "/" + imageOriginY);
// das Koordinatensystem des Canvas entspricht nun dem des Bildes
canvas.translate(imageOriginX, imageOriginY);
for (OverlayIcon icon : overlayIconList) {
// save und restore, um alle Icons einzeln zu verschieben
canvas.save();
// Translation für Icon berechnen
float translateX = icon.getImagePositionX() * zoomScale + icon.getImageOffsetX();
float translateY = icon.getImagePositionY() * zoomScale + icon.getImageOffsetY();
// Log.d("LIV/onDraw_overlayIcons", "image pos " + icon.getImagePositionX() + "/" + icon.getImagePositionY()
// + ", offset " + icon.getImageOffsetX() + "/" + icon.getImageOffsetY() + ", zoomscale " + zoomScale +
// ", translate " + translateX + "/" + translateY);
// Canvas verschieben und Icon zeichnen
canvas.translate(translateX, translateY);
icon.draw(canvas);
canvas.restore();
}
}
}